export const PI_D: number = Math.PI * 2.0;
export const PI_H: number = Math.PI / 2.0;
export const PI_Q: number = Math.PI / 4.0;
export const RAD_DEG: number = 180.0 / Math.PI;
export const DEG_RAD: number = Math.PI / 180.0;

export interface Position {
    x: number;
    y: number;
};

export function normalizeDegree(value: number): number {
    value = (value + Math.PI) % (PI_D);
    value += value > 0.0 ? -Math.PI : Math.PI;

    return value;
}

export function normalizeRadian(value: number): number {
    value = (value + 180.0) % (180.0 * 2.0);
    value += value > 0.0 ? -180.0 : 180.0;

    return value;
}

export function distance(pA: Position, pB: Position): number {
    const dX = pB.x - pA.x;
    const dY = pB.y - pA.y;

    return Math.sqrt(dX * dX + dY * dY);
}

export function multiply(pA: Position, pB: Position, pC: Position): number {
    return ((pA.x - pC.x) * (pB.y - pC.y) - (pB.x - pC.x) * (pA.y - pC.y));
}

export class Matrix {
    public constructor(
        public a: number = 1.0, public b: number = 0.0,
        public c: number = 0.0, public d: number = 1.0,
        public tx: number = 0.0, public ty: number = 0.0
    ) {
    }
    /**
     * @language zh_CN
     * 复制矩阵。
     * @param value 需要复制的矩阵。
     * @version DragonBones 3.0
     */
    public copyFrom(value: Matrix): Matrix {
        this.a = value.a;
        this.b = value.b;
        this.c = value.c;
        this.d = value.d;
        this.tx = value.tx;
        this.ty = value.ty;

        return this;
    }

    public copyFromArray(value: number[], offset: number = 0): Matrix {
        this.a = value[offset];
        this.b = value[offset + 1];
        this.c = value[offset + 2];
        this.d = value[offset + 3];
        this.tx = value[offset + 4];
        this.ty = value[offset + 5];

        return this;
    }
    /**
     * @language zh_CN
     * 转换为恒等矩阵。
     * @version DragonBones 3.0
     */
    public identity(): Matrix {
        this.a = this.d = 1.0;
        this.b = this.c = 0.0;
        this.tx = this.ty = 0.0;

        return this;
    }
    /**
     * @language zh_CN
     * 将当前矩阵与另一个矩阵相乘。
     * @param value 需要相乘的矩阵。
     * @version DragonBones 3.0
     */
    public concat(value: Matrix): Matrix {
        let a = this.a * value.a;
        let b = 0.0;
        let c = 0.0;
        let d = this.d * value.d;
        let tx = this.tx * value.a + value.tx;
        let ty = this.ty * value.d + value.ty;

        if (this.b !== 0.0 || this.c !== 0.0) {
            a += this.b * value.c;
            d += this.c * value.b;
            b += this.b * value.d;
            c += this.c * value.a;
        }

        if (value.b !== 0.0 || value.c !== 0.0) {
            b += this.a * value.b;
            c += this.d * value.c;
            tx += this.ty * value.c;
            ty += this.tx * value.b;
        }

        this.a = a;
        this.b = b;
        this.c = c;
        this.d = d;
        this.tx = tx;
        this.ty = ty;

        return this;
    }
    /**
     * @language zh_CN
     * 转换为逆矩阵。
     * @version DragonBones 3.0
     */
    public invert(): Matrix {
        const aA = this.a;
        const bA = this.b;
        const cA = this.c;
        const dA = this.d;
        const txA = this.tx;
        const tyA = this.ty;
        const n = aA * dA - bA * cA;

        this.a = dA / n;
        this.b = -bA / n;
        this.c = -cA / n;
        this.d = aA / n;
        this.tx = (cA * tyA - dA * txA) / n;
        this.ty = -(aA * tyA - bA * txA) / n;

        return this;
    }
    /**
     * @language zh_CN
     * 将矩阵转换应用于指定点。
     * @param x 横坐标。
     * @param y 纵坐标。
     * @param result 应用转换之后的坐标。
     * @param delta 是否忽略 tx，ty 对坐标的转换。
     * @version DragonBones 3.0
     */
    public transformPoint(x: number, y: number, result: Position, delta: boolean = false): void {
        result = result || new Point();
        result.x = this.a * x + this.c * y;
        result.y = this.b * x + this.d * y;

        if (!delta) {
            result.x += this.tx;
            result.y += this.ty;
        }
    }
}
/**
 * 
 */
export class Point implements Position {
    public constructor(
        public x: number = 0.0,
        public y: number = 0.0
    ) {
    }

    public toString(): string {
        return "[object Point x: " + this.x + " y: " + this.y + " ]";
    }

    public clear(): void {
        this.x = this.y = 0.0;
    }

    public toFixed(fractionDigits: number): void {
        this.x = Number(this.x.toFixed(fractionDigits));
        this.y = Number(this.y.toFixed(fractionDigits));
    }

    public copyFrom(value: Position): this {
        this.x = value.x;
        this.y = value.y;

        return this;
    }

    public setTo(x: number, y: number): this {
        this.x = x;
        this.y = y;

        return this;
    }

    public equal(value: Position): boolean {
        return this.x === value.x && this.y === value.y;
    }

    public polar(length: number, radian: number): this {
        this.x = length * Math.cos(radian);
        this.y = length * Math.sin(radian);

        return this;
    }
}

export class Rectangle implements Position {
    public constructor(
        public x: number = 0.0,
        public y: number = 0.0,
        public width: number = 0.0,
        public height: number = 0.0
    ) {
    }

    public toString(): string {
        return "[object Rectangle x: " + this.x + " y: " + this.y + " width: " + this.width + " height: " + this.height + " ]";
    }

    public clear(): void {
        this.x = this.y = this.width = this.height = 0.0;
    }

    public toFixed(fractionDigits: number): void {
        this.x = Number(this.x.toFixed(fractionDigits));
        this.y = Number(this.y.toFixed(fractionDigits));
        this.width = Number(this.width.toFixed(fractionDigits));
        this.height = Number(this.height.toFixed(fractionDigits));
    }

    public copyFrom(value: this): this {
        this.x = value.x;
        this.y = value.y;
        this.width = value.width;
        this.height = value.height;

        return this;
    }

    public setTo(x: number, y: number, width: number, height: number): this {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;

        return this;
    }

    public equal(value: this): boolean {
        return this.x === value.x &&
            this.y === value.y &&
            this.width === value.width &&
            this.height === value.height;
    }
}

export class Circle implements Position {
    public constructor(
        public x: number = 0.0,
        public y: number = 0.0,
        public radius: number = 0.0
    ) {
    }

    public toString(): string {
        return "[object Circle x: " + this.x + " y: " + this.y + " radius: " + this.radius + " ]";
    }

    public clear(): void {
        this.x = this.y = this.radius = 0.0;
    }

    public copyFrom(value: this): this {
        this.x = value.x;
        this.y = value.y;
        this.radius = value.radius;

        return this;
    }

    public setTo(x: number, y: number, radius: number): this {
        this.x = x;
        this.y = y;
        this.radius = radius;

        return this;
    }

    public equal(value: this): boolean {
        return this.x === value.x &&
            this.y === value.y &&
            this.radius === value.radius;
    }
}

export class Transform implements Position {
    public constructor(
        public x: number = 0.0,
        public y: number = 0.0,
        public rotation: number = 0.0,
        public skew: number = 0.0,
        public scaleX: number = 1.0,
        public scaleY: number = 1.0
    ) {
    }

    public identity(): Transform {
        this.x = this.y = 0.0;
        this.rotation = this.skew = 0.0;
        this.scaleX = this.scaleY = 0.0;

        return this;
    }

    public toFixed(fractionDigits: number): void {
        this.x = Number(this.x.toFixed(fractionDigits));
        this.y = Number(this.y.toFixed(fractionDigits));
        this.rotation = Number(this.rotation.toFixed(fractionDigits));
        this.skew = Number(this.skew.toFixed(fractionDigits));
        this.scaleX = Number(this.scaleX.toFixed(fractionDigits + 2));
        this.scaleY = Number(this.scaleY.toFixed(fractionDigits + 2));
    }

    public copyFrom(value: Transform): Transform {
        this.x = value.x;
        this.y = value.y;
        this.rotation = value.rotation;
        this.skew = value.skew;
        this.scaleX = value.scaleX;
        this.scaleY = value.scaleY;

        return this;
    }

    public add(value: Transform): Transform {
        this.x += value.x;
        this.y += value.y;
        this.rotation += value.rotation;
        this.skew += value.skew;
        this.scaleX *= value.scaleX;
        this.scaleY *= value.scaleY;

        return this;
    }

    public minus(value: Transform): Transform {
        this.x -= value.x;
        this.y -= value.y;
        this.rotation = normalizeDegree(this.rotation - value.rotation);
        this.skew = this.skew - value.skew;
        this.scaleX /= value.scaleX;
        this.scaleY /= value.scaleY;

        return this;
    }

    public fromMatrix(matrix: Matrix): Transform {
        const backupScaleX = this.scaleX, backupScaleY = this.scaleY;

        this.x = matrix.tx;
        this.y = matrix.ty;

        this.rotation = Math.atan(matrix.b / matrix.a);
        let skewX = Math.atan(-matrix.c / matrix.d);

        if (this.rotation !== this.rotation) {
            this.rotation = 0.0;
        }

        if (skewX !== skewX) {
            skewX = 0.0;
        }

        this.scaleX = (this.rotation > -PI_Q && this.rotation < PI_Q) ? matrix.a / Math.cos(this.rotation) : matrix.b / Math.sin(this.rotation);
        this.scaleY = (skewX > -PI_Q && skewX < PI_Q) ? matrix.d / Math.cos(skewX) : -matrix.c / Math.sin(skewX);

        if (backupScaleX >= 0.0 && this.scaleX < 0.0) {
            this.scaleX = -this.scaleX;
            this.rotation = this.rotation - Math.PI;
        }

        if (backupScaleY >= 0.0 && this.scaleY < 0.0) {
            this.scaleY = -this.scaleY;
            skewX = skewX - Math.PI;
        }

        this.skew = skewX - this.rotation;

        return this;
    }

    public toMatrix(matrix: Matrix): Transform {
        if (this.skew !== 0.0 || this.rotation !== 0.0) {
            matrix.a = Math.cos(this.rotation);
            matrix.b = Math.sin(this.rotation);

            if (this.skew === 0.0) {
                matrix.c = -matrix.b;
                matrix.d = matrix.a;
            }
            else {
                matrix.c = -Math.sin(this.skew + this.rotation);
                matrix.d = Math.cos(this.skew + this.rotation);
            }

            if (this.scaleX !== 1.0 || this.scaleY !== 1.0) {
                matrix.a *= this.scaleX;
                matrix.b *= this.scaleX;
                matrix.c *= this.scaleY;
                matrix.d *= this.scaleY;
            }
        }
        else {
            matrix.a = this.scaleX;
            matrix.b = 0.0;
            matrix.c = 0.0;
            matrix.d = this.scaleY;
        }

        matrix.tx = this.x;
        matrix.ty = this.y;

        return this;
    }

    public equal(value: this): boolean {
        return this.x === value.x &&
            this.y === value.y &&
            this.skew === value.skew &&
            this.rotation === value.rotation &&
            this.scaleX === value.scaleX &&
            this.scaleY === value.scaleY;
    }
}

export class ColorTransform {
    public constructor(
        public aM: number = 100, public rM: number = 100, public gM: number = 100, public bM: number = 100,
        public aO: number = 0, public rO: number = 0, public gO: number = 0, public bO: number = 0
    ) {
    }

    public copyFrom(value: ColorTransform): this {
        this.aM = value.aM;
        this.rM = value.rM;
        this.gM = value.gM;
        this.bM = value.bM;
        this.aO = value.aO;
        this.rO = value.rO;
        this.gO = value.gO;
        this.bO = value.bO;

        return this;
    }

    public concat(value: ColorTransform): this {
        this.aM *= value.aM * 0.01;
        this.rM *= value.rM * 0.01;
        this.gM *= value.gM * 0.01;
        this.bM *= value.bM * 0.01;
        this.aO += value.aO;
        this.rO += value.rO;
        this.gO += value.gO;
        this.bO += value.bO;

        return this;
    }

    public equal(value: ColorTransform): boolean {
        return this.aM === value.aM &&
            this.rM === value.rM &&
            this.gM === value.gM &&
            this.bM === value.bM &&
            this.aO === value.aO &&
            this.rO === value.rO &&
            this.gO === value.gO &&
            this.bO === value.bO;
    }
}